package org.newdawn.yapgame;

import java.awt.Canvas;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.TreeMap;

import javax.swing.JOptionPane;

import org.newdawn.yapgame.java2d.Java2DResourceFactory;
import org.newdawn.yapgame.lwjgl.LWJGLResourceFactory;

/**
 * The main hook of our game. This class with both act as a manager for the
 * display and central mediator for the game logic.
 * 
 * Display management will consist of a loop that cycles round all entities in
 * the game asking them to move and then drawing them in the appropriate place.
 * With the help of an inner class it will also allow the player to control the
 * main ship.
 * 
 * As a mediator it will be informed when entities within our game detect events
 * (e.g. alient killed, played died) and will take appropriate game actions.
 * 
 * @author Kevin Glass
 */
public class Game extends Canvas implements GameWindowCallback
{
    /** */
    private static final long serialVersionUID = 1L;

    /** global static game reference */
    private static Game game = null;
    
    /** Interface to resource factory */
	protected IResourceFactory resFactory = null;
  	
    /** Interface to process manager */
	protected IProcessManager proc_manager = null;

    /** Interface to event manager */
	protected IEventManager event_manager = null;
	
    /** The list of all the entities that exist in our game */
    private ArrayList<Entity> entities = new ArrayList<Entity>();
    /** The list of entities that need to be removed from the game this loop */
    private ArrayList<Entity> removeList = new ArrayList<Entity>();
    /** The list of all screen layers sorted by z-value (lowest key is bottom-most layer) */
    protected TreeMap<Integer, ScreenLayer> screenLayers = new TreeMap<Integer, ScreenLayer>();
    /** The entity representing the player */
    private Entity ship;
    /** The speed at which the player's ship should move (pixels/sec) */
    private double moveSpeed = 300;
    /** The time at which last fired a shot */
    private long lastFire = 0;
    /** The interval between our players shot (ms) */
    private long firingInterval = 500;
    /** The number of aliens left on the screen */
    private int alienCount;

    /** The message to display which waiting for a key press */
    private ISprite message;
    /** True if we're holding up game play until a key has been pressed */
    private boolean waitingForKeyPress = true;
    /**
     * True if game logic needs to be applied this loop, normally as a result of
     * a game event
     */
    private boolean logicRequiredThisLoop = false;

    /**
     * The time at which the last rendering looped started from the point of
     * view of the game logic
     */
    private long lastLoopTime = SystemTimer.getTime();
    /** The window that is being used to render the game */
    private GameWindow window;
    /** True if the fire key has been released */
    private boolean fireHasBeenReleased = false;

    /** The sprite containing the "Press Any Key" message */
    private ISprite pressAnyKey;
    /** The sprite containing the "You win!" message */
    private ISprite youWin;
    /** The sprite containing the "You lose!" message */
    private ISprite gotYou;

    /** The time since the last record of fps */
    private long lastFpsTime = 0;
    /** The recorded fps */
    private int fps;

    /** The normal title of the window */
    private String windowTitle = "Space Invaders 104 - Version (0.4)";
    
    // -----------------------------------------------------------------------
    
    /**
     * Construct our game and set it running.
     * 
     * @param renderingType The type of rendering to use
     */
    public Game( RenderingType rt )
    {    	
    	game = this;
    	switch( rt )
    	{
    		case JAVA2D:
    			resFactory = new Java2DResourceFactory();
    			break;
    		case LWJGL:
    			resFactory = new LWJGLResourceFactory();
    			break;
    	}
		// create a window based on a chosen rendering method
		window = resFactory.getGameWindow();
	
		window.setPreferredResolution(800, 600);
		window.setGameWindowCallback( this );
		window.setTitle( windowTitle );
	
		window.startRendering();
    }

    // -----------------------------------------------------------------------
    
    /**
     * Initialize the common elements for the game
     */
    public void initialize()
    {
    	proc_manager = new ProcessManager();
    	event_manager = new EventManager();
    	
		gotYou = resFactory.getSprite("sprites/gotyou.gif");
		pressAnyKey = resFactory.getSprite("sprites/pressanykey.gif");
		youWin = resFactory.getSprite("sprites/youwin.gif");

		message = pressAnyKey;

		// setup the initial game state
		startGame();
    }

    /**
     * Start a fresh game, this should clear out any old data and create a new
     * set.
     */
    private void startGame()
    {
		// clear out any existing entities and intialise a new set
		entities.clear();
		initEntities();
    }

    /**
     * Initialize the starting state of the entities (ship and aliens). Each
     * entity will be added to the overall list of entities in the game.
     */
    private void initEntities()
    {
		// create the player ship and place it roughly in the center of the
		// screen
		ship = new ShipEntity(this, "sprites/ship.gif", 370, 550);
		entities.add(ship);
	
		// create a block of aliens (5 rows, by 12 aliens, spaced evenly)
		alienCount = 0;
		for (int row = 0; row < 5; row++)
		{
		    for (int x = 0; x < 12; x++)
		    {
			Entity alien = new AlienEntity(this, 100 + (x * 50),
				(50) + row * 30);
			entities.add(alien);
			alienCount++;
		    }
		}
    }

    /**
     * Notification from a game entity that the logic of the game should be run
     * at the next opportunity (normally as a result of some game event)
     */
    public void updateLogic()
    {
    	logicRequiredThisLoop = true;
    }

    /**
     * Remove an entity from the game. The entity removed will no longer move or
     * be drawn.
     * 
     * @param entity
     *            The entity that should be removed
     */
    public void removeEntity(Entity entity)
    {
    	removeList.add(entity);
    }

    /**
     * Notification that the player has died.
     */
    public void notifyDeath()
    {
		message = gotYou;
		waitingForKeyPress = true;
    }

    /**
     * Notification that the player has won since all the aliens are dead.
     */
    public void notifyWin()
    {
		message = youWin;
		waitingForKeyPress = true;
    }

    /**
     * Notification that an alien has been killed
     */
    public void notifyAlienKilled()
    {
		// reduce the alien count, if there are none left, the player has won!
		alienCount--;
	
		if (alienCount == 0)
		{
		    notifyWin();
		}
	
		// if there are still some aliens left then they all need to get faster,
		// so
		// speed up all the existing aliens
		for (int i = 0; i < entities.size(); i++)
		{
		    Entity entity = (Entity) entities.get(i);
	
		    if (entity instanceof AlienEntity)
		    {
		    	// speed up by 2%
		    	entity.setHorizontalMovement(entity.getHorizontalMovement() * 1.02);
		    }
		}
    }

    /**
     * Attempt to fire a shot from the player. Its called "try" since we must
     * first check that the player can fire at this point, i.e. has he/she
     * waited long enough between shots
     */
    public void tryToFire()
    {
		// check that we have waiting long enough to fire
		if (System.currentTimeMillis() - lastFire < firingInterval)
		{
		    return;
		}
	
		// if we waited long enough, create the shot entity, and record the
		// time.
		lastFire = System.currentTimeMillis();
		ShotEntity shot = new ShotEntity(this, "sprites/shot.gif",
			ship.getX() + 10, ship.getY() - 30);
		entities.add(shot);
    }

    /**
     * Notification that a frame is being rendered. Responsible for running game
     * logic and rendering the scene.
     * @throws  
     */
    public void frameRendering() 
    {
		try
		{
		    SystemTimer.sleep(lastLoopTime + 10 - SystemTimer.getTime());
		} 
		catch (InterruptedException e)
		{
		    e.printStackTrace();
		}

		// work out how long its been since the last update, this
		// will be used to calculate how far the entities should
		// move this loop
		long delta = SystemTimer.getTime() - lastLoopTime;
		lastLoopTime = SystemTimer.getTime();
		lastFpsTime += delta;
		fps++;
	
		// update our FPS counter if a second has passed
		if (lastFpsTime >= 1000)
		{
		    window.setTitle( windowTitle+" (FPS: "+fps+")" );
		    lastFpsTime = 0;
		    fps = 0;
		}
		
		// cycle round asking each entity to move itself
		if( !waitingForKeyPress )
		{
		    for (int i = 0; i < entities.size(); i++)
		    {
				Entity entity = (Entity) entities.get(i);
				entity.move(delta);
		    }
		}
		
		// update logic in screen layers
		Iterator<ScreenLayer> iscreens = screenLayers.values().iterator();
		while( iscreens.hasNext() )
		{
			iscreens.next().tick( delta );
		}
		iscreens = null;
			
		// cycle round drawing all the entities we have in the game
		for (int i = 0; i < entities.size(); i++)
		{
		    Entity entity = (Entity) entities.get(i);
		    entity.draw();
		}
	
		// brute force collisions, compare every entity against
		// every other entity. If any of them collide notify
		// both entities that the collision has occured
		for (int p = 0; p < entities.size(); p++)
		{
		    for (int s = p + 1; s < entities.size(); s++)
		    {
			Entity me = (Entity) entities.get(p);
			Entity him = (Entity) entities.get(s);
	
			if (me.collidesWith(him))
			{
			    me.collidedWith(him);
			    him.collidedWith(me);
			}
		    }
		}
	
		// remove any entity that has been marked for clear up
		entities.removeAll(removeList);
		removeList.clear();
	
		// if a game event has indicated that game logic should
		// be resolved, cycle round every entity requesting that
		// their personal logic should be considered.
		if (logicRequiredThisLoop)
		{
		    for (int i = 0; i < entities.size(); i++)
		    {
			Entity entity = (Entity) entities.get(i);
			entity.doLogic();
		    }
	
		    logicRequiredThisLoop = false;
		}
	
		// if we're waiting for an "any key" press then draw the
		// current message
		if (waitingForKeyPress)
		{
		    message.draw(325, 250);
		}

		// render screen layers
		iscreens = screenLayers.values().iterator();
		while( iscreens.hasNext() )
		{
			iscreens.next().render( delta );
		}
		iscreens = null;
		
		// resolve the movement of the ship. First assume the ship
		// isn't moving. If either cursor key is pressed then
		// update the movement appropriately
		ship.setHorizontalMovement(0);
	
		boolean leftPressed = window.isKeyPressed(KeyEvent.VK_LEFT);
		boolean rightPressed = window.isKeyPressed(KeyEvent.VK_RIGHT);
		boolean firePressed = window.isKeyPressed(KeyEvent.VK_SPACE);
	
		if (!waitingForKeyPress)
		{
		    if ((leftPressed) && (!rightPressed))
		    {
			ship.setHorizontalMovement(-moveSpeed);
		    } else if ((rightPressed) && (!leftPressed))
		    {
			ship.setHorizontalMovement(moveSpeed);
		    }
	
		    // if we're pressing fire, attempt to fire
		    if (firePressed)
		    {
			tryToFire();
		    }
		} else
		{
		    if (!firePressed)
		    {
			fireHasBeenReleased = true;
		    }
		    if ((firePressed) && (fireHasBeenReleased))
		    {
			waitingForKeyPress = false;
			fireHasBeenReleased = false;
			startGame();
		    }
		}
	
		// if escape has been pressed, stop the game
		if (window.isKeyPressed(KeyEvent.VK_ESCAPE))
		{
		    System.exit(0);
		}
    }
    
    /**
     * Notification that the game window has been closed
     */
    public void windowClosed()
    {
    	screenLayers.clear();
		System.exit(0);
    }

    /* =======================================================================
     * static methods
     * =====================================================================*/
    
    /**
     * Returns game instance
     * @return game instance
     */
    public static Game get()
    {
    	return game;
    }

    /**
     * Returns process manager of game instance
     * @return  resource factory
     */
    public static IProcessManager getProcessManager()
    {
    	return game.proc_manager;
    }
    
    /**
     * Returns resource factory of game instance
     * @return  resource factory
     */
    public static IResourceFactory getResourceFactory()
    {
    	return game.resFactory;
    }

    
    /**
     * The entry point into the game. We'll simply create an instance of class
     * which will start the display and game loop.
     * 
     * @param argv
     *            The arguments that are passed into our game
     */
    public static void main(String argv[])
    {
    	int result = JOptionPane.showOptionDialog(null, "Java2D or LWJGL?",
		"Java2D or LWJGL?", JOptionPane.YES_NO_CANCEL_OPTION,
		JOptionPane.QUESTION_MESSAGE, null, new String[]
		{ "Java2D", "LWJGL" }, null);

		new Game( RenderingType.valueOf( result ) );
    }
}